feat: Grep and Glob results render as clickable locations - #288
Merged
Conversation
The DSH round specified a fourth render intent — locations — and deferred it: deriving entries by parsing the formatted result text is unsound, because the ':' separator is ambiguous the moment a path contains one. That is the same reason the withhold filter runs on parsed rows. So the entries are attached at the source instead. Grep and Glob already hold exact rows (Grep from ripgrep's --null output) and now publish them as ToolResult.data.locations — absolute path for opening, the printed form for display, line and matched text where the mode knows them, capped at MAX_TOOL_LOCATIONS. The intent stays a pure function of the call's arguments; the entries ride the result's existing data channel, which the server has always serialized verbatim (the protocol type now says so instead of narrowing it away). The desktop reads them through one shared validating extractor (readToolLocations) and renders a search's card as one openable row per hit, wired to the same file-panel opener the card header already uses. A session restored from its log has only the text the model saw, so replayed cards degrade to today's plain-text body — same characters, minus the click. Grep's head_limit now slices rows before formatting (output unchanged byte for byte) so entries line up with shown lines, and the contract tests pin that withheld paths never leak through the structured channel. Verified in the vite preview harness: line-numbered Grep rows with match previews, Glob's relative rows, clicks reporting absolute paths, and the restored-session card falling back to plain text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The fourth render intent from the DSH adoption plan (§1.6), specified there and deferred by #271:
Grep/Globcards in the desktop rendered as a grey text blob; now they render as a list of openable file locations — line number and matched text included in content mode — each row wired to the same file-panel opener the card header already uses.Why parsing the result text was the wrong design (and what this does instead)
The deferral reason was real: re-deriving entries from the formatted result text means parsing
path:line:text, whose:separator is ambiguous the moment a path contains one. Grep itself dodges that ambiguity internally with ripgrep's--nulloutput — throwing the exact rows away and re-parsing the lossy format back would be guesswork presented as UI.So the entries are attached at the source: both tools publish the rows they already hold as
ToolResult.data.locations({path, display?, line?, preview?}, absolute path for opening, printed form for display, capped atMAX_TOOL_LOCATIONS = 200). The render intent stays a pure function of the call's arguments; the entries ride the result's existing structured-data channel, which the app-server has always serialized verbatim —ToolCompletedEvent.resultnow declaresdata?instead of narrowing it away, documenting the wire rather than widening it.Clients share one validating extractor,
readToolLocations, so a malformed payload degrades to "no locations" everywhere instead of crashing one renderer.Honest boundaries
ToolResultBlock.content), so replayed cards fall back to today's plain-text body — same characters, no buttons. Persisting presentation data into thread items is a possible follow-up; it was left out to keep the stored formats untouched.Grep'shead_limitnow slices rows before formatting so entries line up with shown lines — the emitted text is unchanged byte for byte.Verification
typecheck·lint·format:check·build·docs:checkall green.DC_REQUIRE_RIPGREP=1) · desktop 105 passed.preview-toolcards.html), checked in the in-app browser: line-numbered Grep rows with match previews, Glob's relative rows, clicks reporting the absolute path, and a restored-session card (result text, no data) falling back to plain text.🤖 Generated with Claude Code